from typing import NoReturn, Self

import pygame

from vengine.ve_constants import VE_KEYMAP
from vengine.ve_coord import Point2d
from vengine.ve_event import VE_EventData, VE_EventDispatcher
from vengine.ve_exceptions import VE_EXIT_CODE
from vengine.ve_globals import (VE_DATA_STORAGE, KMstate, VE_GroupIdPool,
                                VE_ObjectIdPool, get_group, get_window)
from vengine.ve_group import VE_Group
from vengine.ve_logger import get_logger
from vengine.ve_objects import VE_Clickable


class Application:
    """ Entrypoint of the engine """
    
    _instance = None
    
    def __init__(
        self,
        
        resolution: tuple[float, float] = (0, 0),
        fps: int = 60,
    
        logging_level: int | str = "INFO"
    ) -> None:
        """Initialize vEngine
        
        Args:
            resolution (tuple[float, float], optional): Window resolution. Defaults to fullscreen (0, 0).
            fps (int, optional): Game / Window fps. Defaults to 60.
            
            logging_level (int | str, optional): Defaults to "INFO".
        """
                
        # Initialize root logger
        get_logger(level=logging_level, fmt="%(log_color)s[%(asctime)s] [vEngine/%(levelname)s] %(message)s")

        if VE_DATA_STORAGE["is_initialized"]:
            get_logger().error("Creating mutiple application is not allowed", stack_info=True)
            exit(VE_EXIT_CODE.MUTIPLE_APP)

        get_logger().debug("Initializing")
        
        self.resolution = resolution
        self.fps = fps
        self.logging_level = logging_level

        # Pygame setup
        pygame.init()
        
        self.window = pygame.display.set_mode(resolution)        
        VE_DATA_STORAGE["window"] = self.window
        
        self.clock = pygame.time.Clock()
        VE_DATA_STORAGE["clock"] = self.clock
        
        self._init_dynamic()
        
        self.__class__._instance = self
        self.__class__.get_instance = self.__class__._get_instance2

        VE_DATA_STORAGE["is_initialized"] = True
        VE_EventDispatcher.dispatch("VE_after_init")
    
    @classmethod
    def _init_dynamic(cls) -> None:
        VE_ObjectIdPool.clear()
        VE_GroupIdPool.clear()
        
        # Creating all groups
        VE_Group("Visible")
        
        # Set all key to unpressed
        for _, member in VE_KEYMAP._member_map_.items():
            KMstate[member.value] = False
    
    @classmethod
    def reset(cls) -> None:
        """ Completely reset the engine back to init stage """
        cls._init_dynamic()
        
    def _handle_pygame_event(self, event: pygame.event.Event) -> None:
        match event.type:
            case pygame.QUIT:
                VE_EventDispatcher.dispatch("VE_quit", VE_EventData.VE_quit({
                    "exit_code" : 1
                }))
                exit(VE_EXIT_CODE.SUCCESS)

            case pygame.KEYDOWN:
                e = {"key": event.key, "mod": event.mod}
                if not KMstate[event.key]:
                    KMstate[event.key] = True
                    VE_EventDispatcher.dispatch("K_press", VE_EventData.K_press(e)) # type: ignore
                else:
                    VE_EventDispatcher.dispatch("K_down", VE_EventData.K_down(e)) # type: ignore

            case pygame.KEYUP:
                e = {"key": event.key, "mod": event.mod}
                if KMstate[event.key]:
                    KMstate[event.key] = False
                    VE_EventDispatcher.dispatch("K_release", VE_EventData.K_release(e)) # type: ignore
                else:
                    VE_EventDispatcher.dispatch("K_up", VE_EventData.K_up(e)) # type: ignore
                
            case pygame.MOUSEBUTTONDOWN:
                e = {"button": event.button, "pos": event.pos}
                if not KMstate[event.button]:
                    KMstate[event.button] = True
                    VE_EventDispatcher.dispatch("M_press", VE_EventData.M_press(e)) # type: ignore
                else:
                    VE_EventDispatcher.dispatch("M_down", VE_EventData.M_down(e)) # type: ignore

            case pygame.MOUSEBUTTONUP:
                e = {"button": event.button, "pos": event.pos}
                if KMstate[event.button]:
                    KMstate[event.button] = False
                    VE_EventDispatcher.dispatch("M_release", VE_EventData.M_release(e)) # type: ignore
                else:
                    VE_EventDispatcher.dispatch("M_up", VE_EventData.M_up(e)) # type: ignore
                
            case pygame.MOUSEMOTION:
                VE_EventDispatcher.dispatch("M_motion", VE_EventData.M_motion({
                    "buttons" : event.buttons,
                    "pos" : event.pos,
                    "rel" : event.rel
                }))
        
    def run_forever(self) -> NoReturn:
        """ Run the engine forever """
        
        if not VE_DATA_STORAGE["is_initialized"]:
            get_logger().error("Called run_forever() before initializing vEngine")
            exit(VE_EXIT_CODE.RUN_BEFORE_INITIALIZE)
            
        while True:
            self.clock.tick(self.fps)
            
            # Clear window
            get_window().fill((255, 255, 255))
            
            for event in pygame.event.get():
                self._handle_pygame_event(event)
                        
            VE_EventDispatcher.dispatch("VE_display")
            for object_id, obj in get_group("Visible"):
                obj.display()
            
            # Update window display
            pygame.display.flip()
            
            VE_EventDispatcher.dispatch("VE_tick")
            for object_id, obj in list(VE_ObjectIdPool.items()):
                obj.tick()
            
    @classmethod
    def get_instance(cls) -> Self:
        if VE_DATA_STORAGE["is_initialized"]:
            return cls._instance # type: ignore
        get_logger().error("Called get_instance() before initializing vEngine")
        exit(VE_EXIT_CODE.RUN_BEFORE_INITIALIZE)
        
    @classmethod
    def _get_instance2(cls) -> Self:
        return cls._instance # type: ignore


@VE_EventDispatcher.event("M_press")
def _(e: VE_EventData.M_press) -> None:
    for object_id, obj in list(VE_ObjectIdPool.items()):
        if not isinstance(obj, VE_Clickable): continue
        if not obj.in_click_area(Point2d(e["pos"])): continue
        
        VE_EventDispatcher.dispatch(f"{object_id}.click", VE_EventData.object_click({
            "object" : obj,
            "pos" : Point2d(e["pos"]),
            "button" : VE_KEYMAP(e["button"])
        }))